package codegen;

import parser.Symboltable;

import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;

public class Generator {

    public static final String DELIMITER = ",";
    public static final int OPCODE_EXIT = 0;
    public static final int OPCODE_IN = 1;
    public static final int OPCODE_OUT = 2;
    public static final int OPCODE_PLUS = 3;
    public static final int OPCODE_SUB = 4;
    public static final int OPCODE_MULT = 5;
    public static final int OPCODE_DIV = 6;
    public static final int OPCODE_LESS = 7;
    public static final int OPCODE_EQUAL = 8;
    public static final int OPCODE_OR = 9;
    public static final int OPCODE_AND = 10;
    public static final int OPCODE_NEG = 11;
    public static final int OPCODE_LOAD = 12;
    public static final int OPCODE_SAVE = 13;
    public static final int OPCODE_JUMP = 14;
    public static final int OPCODE_CONDJUMP = 15;
    public static final int OPCODE_LOAD_GEN = 100;
    public static final int OPCODE_MULT_GEN = 101;

    public static final int AN = 0;
    public static final int AE = 1;
    public static final int ANE = -1;

    private static final int DEFAULT_ADDRESS = 0;
    private final Symboltable symboltable;
    private final LinkedList<Operation> code;
    private final LinkedHashMap<Integer, Integer> constTable;
    private final IdentityHashMap<Integer, Integer> jumpTable;
    private int instructionCounter;
    private int constantValueCounter;
    private List<Integer> constValues;
    private int jumpCounter;

    public Generator(final Symboltable symboltable) {
        this.instructionCounter = 0;
        this.constantValueCounter = 0;
        this.code = new LinkedList<Operation>();
        this.symboltable = symboltable;
        this.constTable = new LinkedHashMap<Integer, Integer>();
        this.constValues = new LinkedList<>();
        this.jumpTable = new IdentityHashMap<>();
        this.jumpCounter = DEFAULT_ADDRESS;
        fillConstValuesWithDefaultValues();
    }

    public void addJumpDest(final int jump, final int dest) {
        this.jumpTable.put(jump, dest);
    }

    public int getJumpCounter() {
        final int result = this.jumpCounter;
        this.jumpCounter += 1;
        return result;
    }

    public int getInstructionCounter() {
        return this.instructionCounter;
    }

    private void fillConstValuesWithDefaultValues() {
        addValueToConstValueTable(AE);
        addValueToConstValueTable(ANE);
        addValueToConstValueTable(AN);
    }

    private void out(final int opcode, final int adress) {
        this.instructionCounter += 1;
        this.code.add(new Operation(opcode, adress));
    }

    public void generate_print() {
        out(OPCODE_OUT, DEFAULT_ADDRESS);
    }

    public void generate_exit() {
        out(OPCODE_EXIT, DEFAULT_ADDRESS);
    }

    public void generate_in(final String key) {
        out(OPCODE_IN, DEFAULT_ADDRESS);
        out(OPCODE_SAVE, this.symboltable.getRegister(key));
    }

    public void generate_load(final String key) {
        out(OPCODE_LOAD, this.symboltable.getRegister(key));
    }

    public void generate_constValue(final int number) {
        if (!this.constTable.containsKey(number)) {
            addValueToConstValueTable(number);
        }
        out(OPCODE_LOAD_GEN, this.constTable.get(number));
    }

    private void addValueToConstValueTable(int number) {
        this.constTable.put(number, this.constantValueCounter);
        this.symboltable.increaseNextFree();
        this.constantValueCounter += 1;
        this.constValues.add(number);
    }

    public void generate_assign(final String key) {
        out(OPCODE_SAVE, this.symboltable.getRegister(key));
    }

    public int generate_store() {
        final int address = this.symboltable.getNextFree();
        this.symboltable.increaseNextFree();
        out(OPCODE_SAVE, address);
        return address;
    }

    public void generate_addition(final int left, final int right) {
        out(OPCODE_LOAD, left);
        out(OPCODE_PLUS, right);
    }

    public void generate_subtraction(final int left, final int right) {
        out(OPCODE_LOAD, left);
        out(OPCODE_SUB, right);
    }

    public void generate_multiplication(final int left, final int right) {
        out(OPCODE_LOAD, left);
        out(OPCODE_MULT, right);
    }

    public void generate_division(final int left, final int right) {
        out(OPCODE_LOAD, left);
        out(OPCODE_DIV, right);
    }

    public void generate_neg() {
        out(OPCODE_MULT_GEN, this.constTable.get(ANE));
    }

    public void generate_logneg() {
        out(OPCODE_NEG, DEFAULT_ADDRESS);
    }

    public void generate_and(final int left, final int right) {
        out(OPCODE_LOAD, left);
        out(OPCODE_AND, right);
    }

    public void generate_or(final int left, final int right) {
        out(OPCODE_LOAD, left);
        out(OPCODE_OR, right);
    }

    public void generate_less(final int left, final int right) {
        out(OPCODE_LOAD, right);
        out(OPCODE_LESS, left);
    }

    public void generate_equal(final int left, final int right) {
        out(OPCODE_LOAD, left);
        out(OPCODE_EQUAL, right);
    }

    public void generate_constFalse() {
        out(OPCODE_LOAD_GEN, this.constTable.get(AN));
    }

    public void generate_constTrue() {
        out(OPCODE_LOAD_GEN, this.constTable.get(AE));
    }

    public void generate_jump(final int jump) {
        out(OPCODE_JUMP, jump);
    }

    public void generate_condJump(final int jump) {
        out(OPCODE_CONDJUMP, jump);
    }

    public String toString() {
        final StringBuilder sb = new StringBuilder();

        for (final Operation op : this.code) {
            if (OPCODE_LOAD_GEN == op.getOpCode()) {
                sb.append(OPCODE_LOAD);
                sb.append(DELIMITER);
                sb.append((op.getAdress() + this.instructionCounter) * 2);
            } else if (OPCODE_MULT_GEN == op.getOpCode()) {
                sb.append(OPCODE_MULT);
                sb.append(DELIMITER);
                sb.append((op.getAdress() + this.instructionCounter) * 2);
            } else if ((OPCODE_CONDJUMP == op.getOpCode()) || OPCODE_JUMP == op.getOpCode()) {
                sb.append(op.getOpCode());
                sb.append(DELIMITER);
                sb.append(this.jumpTable.get(op.getAdress()));
            } else {
                sb.append(op.getOpCode());
                sb.append(DELIMITER);
                if ((op.getOpCode() == OPCODE_EXIT) || (op.getOpCode() == OPCODE_OUT) || (op.getOpCode() == OPCODE_NEG)) {
                    sb.append(op.getAdress());
                } else {
                    sb.append((op.getAdress() + this.instructionCounter + this.constantValueCounter) * 2);
                }
            }
            sb.append(DELIMITER);
        }

        for (final int constValue : this.constValues) {
            sb.append(constValue);
            sb.append(DELIMITER);
            sb.append(DEFAULT_ADDRESS);
            sb.append(DELIMITER);
        }

        return sb.toString();
    }
}
